<script>
  const _toString = Object.prototype.toString

  function isDef(v) {
    return v !== undefined && v !== null
  }

  function isAsyncPlaceholder(node) {
    return node.isComment && node.asyncFactory
  }

  function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
      for (let i = 0; i < children.length; i++) {
        const c = children[i]
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
          return c
        }
      }
    }
  }
  function isRegExp(v) {
    return _toString.call(v) === '[object RegExp]'
  }

  function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
  }

  function matches(pattern, name) {
    if (Array.isArray(pattern)) {
      return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
      return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {
      return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
  }

  function remove(arr, item) {
    if (arr.length) {
      const index = arr.indexOf(item)
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }

  function pruneCache(keepAliveInstance, filter) {
    const { cache, keys, _vnode } = keepAliveInstance
    for (const key in cache) {
      const entry = cache[key]
      if (entry) {
        const name = entry.name
        if (name && !filter(name)) {
          pruneCacheEntry(cache, key, keys, _vnode)
        }
      }
    }
  }

  function pruneCacheEntry(cache, key, keys, current) {
    const entry = cache[key]
    if (entry && (!current || entry.tag !== current.tag)) {
      entry.componentInstance.$destroy()
    }
    cache[key] = null
    remove(keys, key)
  }
  export default {
    name: 'KeepViewAlive',
    abstract: true,

    props: {
      include: Array,
      exclude: Array,
      max: [String, Number]
    },

    created() {
      this.cache = Object.create(null)
      this.keys = []
    },

    destroyed() {
      for (const key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys)
      }
    },

    mounted() {
      this.cacheVNode()
      this.$watch('include', val => {
        pruneCache(this, name => matches(val, name))
      })
      this.$watch('exclude', val => {
        pruneCache(this, name => !matches(val, name))
      })
    },

    updated() {
      this.cacheVNode()
    },

    methods: {
      cacheVNode() {
        const { cache, keys, vnodeToCache, keyToCache } = this
        if (vnodeToCache) {
          const { tag, componentInstance, componentOptions } = vnodeToCache
          cache[keyToCache] = {
            name: getComponentName(componentOptions),
            tag,
            cid: componentOptions.Ctor.cid,
            componentInstance
          }
          keys.push(keyToCache)
          // prune oldest entry
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode)
          }
          this.vnodeToCache = null
        }
      }
    },

    render() {
      const slot = this.$slots.default
      const vnode = getFirstComponentChild(slot)
      const componentOptions = vnode && vnode.componentOptions
      if (componentOptions) {
        // check pattern
        const name = getComponentName(componentOptions)
        const { include, exclude } = this
        if (
          // not included
          (include && (!name || !matches(include, name))) ||
          // excluded
          (exclude && name && matches(exclude, name))
        ) {
          return vnode
        }

        const { cache, keys } = this
        const key =
          vnode.key == null
            ? // same constructor may get registered as different local components
              // so cid alone is not enough (#3269)
              componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
        const entry = cache[key]
        if (entry) {
          if (entry.cid === componentOptions.Ctor.cid) {
            vnode.componentInstance = entry.componentInstance
            // make current key freshest
            remove(keys, key)
            keys.push(key)
          } else {
            entry.componentInstance.$destroy()
            cache[key] = null
            // delay setting the cache until update
            this.vnodeToCache = vnode
            this.keyToCache = key
          }
        } else {
          // delay setting the cache until update
          this.vnodeToCache = vnode
          this.keyToCache = key
        }

        vnode.data.keepAlive = true
      }
      return vnode || (slot && slot[0])
    }
  }
</script>
